/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
**/

package com.chasekernan.hxnova.core.races;

import com.chasekernan.hxnova.core.races.ResearchCosts;
import haxe.xml.Check;

//TODO: Possibly move TechLevel to players package?

/**
    TechLevel contains the individual levels achieved in earch field as well as
    some cost methods.
**/
class TechLevel {
    
    /**
        The maximum level achievable for a tech field (capped so miniturization
        isn't infinite.
    **/
    public static var MAX_LEVEL : Int = 26;
    
    /**
        The cost of each other level in other fields that is added to the cost
        for an individual field.
    **/
    public static var LEVEL_COST : Int = 10;
    
    /**
        Tech costs are based on the fibonacci sequence multiplied by this num.
    **/
    public static var FIBONACCI_ADJ : Int = 5;
    
    public var values(default, null) : Array<Int>;
    
    /**
        Start level is the starting level of every field.
    **/
    public function new(?startLevel = 0) {
        if(startLevel < 0 || startLevel > MAX_LEVEL) 
                throw "Start level cannot be less than zero or greater than " +
                      "MAX_LEVEL.";
        
        values = new Array<Int>();
        for(i in 0...ResearchCosts.TECH_TYPES.length) 
            values.push(startLevel);
    }
    
    /**
        Returns the level achieved in a field.
        Throws an error if there is no such tech field.
    **/
    public function getLevel(type : String) : Int {
        //find value
        for(i in 0...ResearchCosts.TECH_TYPES.length) {
            if(ResearchCosts.TECH_TYPES[i] == type) return values[i];
        }
        
        throw "No such tech type: "+type;
    }
    
    /**
        Checks to see if an tech level is within bounds.
    **/
    public static function checkValue(value : Int) : Bool {
        return value >= 0 && value <= MAX_LEVEL;
    }
    
    /**
        Sets the specified tech field to the given value.
        Throws an error if there is no such tech type or the value is out of 
        range.
    **/
    public function setLevel(type : String, value : Int) : Void {
        if(!checkValue(value)) throw "Value is out of range: "+value;
        
        for(i in 0...ResearchCosts.TECH_TYPES.length) {
            if(ResearchCosts.TECH_TYPES[i] == type) {values[i] = value; return;}
            
        }
        
        //if here, no such type
        throw "No such tech type: "+type;
    }
    
    /**
        Increases a specific level by 1.
    **/
    public function increaseLevel(type : String) : Void {
        setLevel(type, getLevel(type) + 1);
    }
    
    /**    
        Returns the cost to research the next level in a specfic field.
        Will return -1 if the max level is already achieved.
    **/
    public function getCostForIncrease(field : String, race : Race) : Int {
        if(getLevel(field) == MAX_LEVEL) return -1;
        
        var cost : Float = 0.0;
        for(i in values) cost += i * LEVEL_COST;
        cost += fibonacci(getLevel(field) + FIBONACCI_ADJ) * LEVEL_COST;
        cost *= ResearchCosts.getValue(race.researchCosts.getCost(field));
        return Std.int(cost);
    }
    
    /**
        Returns true if every value in this tech level is <= the one 
        specified.
    **/
    public function isLessThanOrEqualTo(level : TechLevel) : Bool {
        for (techType in ResearchCosts.TECH_TYPES) {
            if (getLevel(techType) > level.getLevel(techType)) return false;
        }
        
        return true;
    }
    
    /**
        Returns true if every value is equal to the values in the other tech
        level.
    **/
    public function isEqualTo(level : TechLevel) : Bool {
        for (techType in ResearchCosts.TECH_TYPES) {
            if (getLevel(techType) != level.getLevel(techType)) return false;
        }
        
        return true;
    }
    
    /**
        Returns true if every value is >= that of the other tech 
        level.
    **/
    public function isGreaterThanOrEqualTo(level : TechLevel) : Bool {
        for (techType in ResearchCosts.TECH_TYPES) {
            if (getLevel(techType) < level.getLevel(techType)) return false;
        }
        
        return true;
    }
    
    /**
        Returns the names of the fields of tech where this has a higher level 
        than the one provided.
    **/
    public function getAdditionalTech(level : TechLevel) : Array < String > {
        var fields = new Array<String>();
        for (field in ResearchCosts.TECH_TYPES) {
            if (getLevel(field) > level.getLevel(field)) fields.push(field);
        }
        
        return fields;
    }
    
    /**
        Returns the cost to achieve a specific tech level based on a race's
        research cost and current tech level.
    **/
    public function getCostToLevel(level : TechLevel, race : Race) : Int {
        //store this so we don't change the actual tech level
        var archive = clone();
        
        var cost : Int = 0;
        
        for(i in ResearchCosts.TECH_TYPES) {
            if(archive.getLevel(i) >= level.getLevel(i)) continue;
            for(j in archive.getLevel(i)...level.getLevel(i)) {
                cost += archive.getCostForIncrease(i, race);
                archive.increaseLevel(i);
            }
        }
        return cost;
    }
    
    private static function fibonacci(n : Int) : Int {
        if(n < 2) return n;
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
    
    public static var XML_TITLE = "tech_level";
    
    public static function getXmlRules() :Rule {
        var indiv : Array<Rule> = new Array<Rule>();
        for(i in ResearchCosts.TECH_TYPES) {
            indiv.push(Rule.RNode(i, [Attrib.Att("level")]));
        }
        
        return Rule.RNode(XML_TITLE, [], Rule.RList(indiv, false));
    }
    
    /**
        Writes a tech level to xml.
        Sample XML:
            <tech_level>
                <energy level="4">
                <weapons level="8">
                <construction level="7">
                <propulsion level="5">
                ...
            </tech_level>
    **/
    public static function writeToXml(tl : TechLevel) : Xml {
        var xml : Xml = Xml.parse('<' + XML_TITLE + '></' + XML_TITLE + '>')
                .firstChild();
                
        for(i in ResearchCosts.TECH_TYPES) {
            var childXml = Xml.createElement(i);
            childXml.set('level', Std.string(tl.getLevel(i)));
            xml.addChild(childXml);
        }
        //check it 
        Check.checkNode(xml, getXmlRules());
        return xml;
    }
    
    /**
        Reads from a tech level xml file. Throws an error if there are any 
        mistakes.
    **/
    public static function readFromXml(xml : Xml) : TechLevel {
        try {
            var tl = new TechLevel();
            //check type
            Check.checkNode(xml, getXmlRules());
            
            for(i in xml.elements()) {
                tl.setLevel(i.nodeName, Std.parseInt(i.get('level')));
            }
                
            return tl;
        } catch (e : Dynamic) {
            throw "Bad tech level xml file: " + e;
        }
    }
    
    public function iterator() {
        return values.iterator();
    }
    
    public function clone() : TechLevel {
        var t = new TechLevel();
        t.values = values.copy();
        return t;
    }
}

#if unit
import haxe.unit.TestCase;

class TechLevelTest extends haxe.unit.TestCase {
    public var tech : TechLevel;
    
    public function testTechLevel() {
        try {
            tech = new TechLevel( -1);
            assertTrue(false);
        } catch (e : Dynamic) {
            assertTrue(true);
        }
        
        tech = new TechLevel(4);
        
        for (techType in ResearchCosts.TECH_TYPES) {
            assertEquals(tech.getLevel(techType), 4);
        }
        
        tech.setLevel(ResearchCosts.TECH_TYPES[0], 7);
        assertEquals(tech.getLevel(ResearchCosts.TECH_TYPES[0]), 7);
        
        try {
            tech.setLevel(ResearchCosts.TECH_TYPES[0], 
                          TechLevel.MAX_LEVEL + 3);
            assertTrue(false);
        } catch (e : Dynamic) {
            assertTrue(true);
        }
        
        try {
            tech.getLevel("gibberish");
            assertTrue(false);
        } catch (e : Dynamic) {
            assertTrue(true);
        }
        
        var clone = tech.clone();
        assertTrue(tech.isEqualTo(clone));
        
        tech.increaseLevel(ResearchCosts.TECH_TYPES[1]);
        
        assertTrue(tech.isGreaterThanOrEqualTo(clone));
        assertTrue(clone.isLessThanOrEqualTo(tech));
        
        tech.increaseLevel(ResearchCosts.TECH_TYPES[2]);
        var increasedTypes = tech.getAdditionalTech(clone);
        
        for (type in increasedTypes) {
            if (type != ResearchCosts.TECH_TYPES[1] &&
                    type != ResearchCosts.TECH_TYPES[2]) assertTrue(false);
        }
        
        tech = new TechLevel(0);
        
        assertEquals(50, tech.getCostForIncrease( 
                ResearchCosts.TECH_TYPES[0], new Race()));
                
        clone = tech.clone();
        clone.setLevel(ResearchCosts.TECH_TYPES[0], 3);
        
        assertEquals(tech.getCostToLevel(clone, new Race()), 290);
        
        tech.increaseLevel(ResearchCosts.TECH_TYPES[1]);
        
        assertEquals(tech.getCostToLevel(clone, new Race()), 320);
    }
    
    public function testTechLevelXml() {
        tech = new TechLevel(9);
        
        var xml = TechLevel.writeToXml(tech);
        for (element in xml.elements()) {
            assertEquals(tech.getLevel(element.nodeName), 
                    Std.parseInt(element.get("level")));
        }
        
        var readValues = TechLevel.readFromXml(xml);
        assertTrue(tech.isEqualTo(readValues));
    }
}

#end